昨天我們成功把 AI 多輪對話紀錄保存到 H2 Database。
原本只要重新啟動 Spring Boot,所有聊天紀錄就會消失。
但現在透過 Spring JDBC,我們已經可以把使用者問題和 AI 回覆保存到資料庫,甚至在重新啟動程式後,繼續之前的對話。
不過我發現,現在的功能還有一個問題。
我們可以保存對話,卻沒有辦法方便地管理對話。
例如,假設使用者已經建立了三段聊天:
對話 A:BMI 是什麼?
對話 B:每天應該喝多少水?
對話 C:如何養成規律作息?
目前如果沒有記住 conversationId,就很難找到之前的對話。
而且即使想刪除某段聊天,也只能手動進入 H2 Console 操作資料庫。
這對使用者來說實在不太方便 XD
所以今天決定加入:
AI 對話管理功能!
今天的目標是:
使用 Spring Boot 和 Spring JDBC,建立對話列表查詢、完整聊天紀錄查詢,以及對話刪除 API,讓 AI 健康助理具備基本的聊天紀錄管理能力。
目前我們已經有:
POST /health/ai/conversation
可以建立新對話或接續既有對話。
今天準備新增三個 API。
| HTTP Method | API | 功能 |
|---|---|---|
| GET | /health/ai/conversations |
查詢所有對話 |
| GET | /health/ai/conversations/{id} |
查詢指定對話的完整紀錄 |
| DELETE | /health/ai/conversations/{id} |
刪除指定對話 |
例如,使用者想查看所有對話:
GET /health/ai/conversations
就可以取得對話列表。
如果想查看某段聊天:
GET /health/ai/conversations/550e8400-e29b-41d4-a716-446655440000
就可以取得完整的問題和回答。
如果想刪除:
DELETE /health/ai/conversations/550e8400-e29b-41d4-a716-446655440000
就可以刪除該段對話及其訊息。
今天我們會沿用 Day 26 的資料庫,不需要重新建立專案。
昨天我們建立了兩張資料表:
conversations
以及:
conversation_messages
它們之間是一對多關係。
conversations
│
├── Conversation A
│ ├── User Message
│ ├── Assistant Message
│ ├── User Message
│ └── Assistant Message
│
└── Conversation B
├── User Message
└── Assistant Message
其中 conversations 負責保存對話資訊:
| 欄位 | 說明 |
|---|---|
| id | 對話識別碼 |
| created_at | 對話建立時間 |
而 conversation_messages 負責保存每一則訊息:
| 欄位 | 說明 |
|---|---|
| id | 訊息流水號 |
| conversation_id | 所屬對話 |
| role | user 或 assistant |
| content | 訊息內容 |
| created_at | 訊息建立時間 |
今天不需要修改這兩張資料表。
我們只要透過 SQL,就可以取得需要的資料。
今天會延續之前的分層設計。
使用者
↓
AIController
↓
ConversationManagementService
↓
ConversationRepository
↓
H2 Database
其中每一層的工作如下:
| 層級 | 負責工作 |
|---|---|
| Controller | 接收 HTTP Request、回傳 Response |
| Service | 處理查詢與刪除流程 |
| Repository | 執行 SQL |
| DTO | 定義回傳資料格式 |
| Database | 保存對話與訊息 |
今天準備新增:
ConversationListResponse.java
ConversationDetailResponse.java
ConversationManagementService.java
ConversationNotFoundException.java
並修改:
ConversationMessage.java
ConversationRepository.java
AIController.java
GlobalExceptionHandler.java
這次不需要修改 AI 模型的呼叫方式。
因為今天主要處理的是資料庫中的聊天紀錄。
假設使用者已經建立了三段對話。
我們希望呼叫:
GET /health/ai/conversations
可以回傳:
[
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2026-10-17T14:00:00",
"messageCount": 4
},
{
"conversationId": "9c1e4b20-2f7a-4c88-a2d3-123456789abc",
"createdAt": "2026-10-17T14:05:00",
"messageCount": 2
}
]
這裡包含三個欄位:
conversationId
createdAt
messageCount
其中 messageCount 代表這段對話總共有幾則訊息。
例如:
User:什麼是 BMI?
Assistant:BMI 是身體質量指數。
User:那它怎麼計算?
Assistant:體重除以身高的平方。
總共就是:
4 則訊息
而不是四輪對話。
這點要特別注意。
首先在:
dto
Package 裡面新增:
ConversationListResponse.java
完整程式如下:
package com.example.healthassistant.dto;
import java.time.LocalDateTime;
public class ConversationListResponse {
private String conversationId;
private LocalDateTime createdAt;
private long messageCount;
public ConversationListResponse(
String conversationId,
LocalDateTime createdAt,
long messageCount) {
this.conversationId = conversationId;
this.createdAt = createdAt;
this.messageCount = messageCount;
}
public String getConversationId() {
return conversationId;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public long getMessageCount() {
return messageCount;
}
}
這裡出現了一個新的 Java 型別:
LocalDateTime
之前我們經常使用:
LocalDate
例如:
2026-10-17
但 LocalDateTime 可以同時保存日期與時間。
例如:
2026-10-17T14:00:00
這樣就可以顯示對話建立的時間。
不過 LocalDateTime 本身不包含時區資訊。
今天沿用 H2 的 TIMESTAMP 欄位,正式系統則需要更明確地管理時區。
接下來打開:
ConversationRepository.java
新增以下 Import:
import com.example.healthassistant.dto.ConversationListResponse;
再加入一個 Method:
public List<ConversationListResponse> findAllConversations() {
String sql = """
SELECT
c.id,
c.created_at,
COUNT(m.id) AS message_count
FROM conversations c
LEFT JOIN conversation_messages m
ON c.id = m.conversation_id
GROUP BY c.id, c.created_at
ORDER BY c.created_at DESC, c.id DESC
""";
return jdbcTemplate.query(
sql,
(rs, rowNum) -> new ConversationListResponse(
rs.getString("id"),
rs.getTimestamp("created_at")
.toLocalDateTime(),
rs.getLong("message_count")
)
);
}
這段 SQL 比之前稍微複雜一點。
因為我們這次不只要查詢對話,也想知道每段對話有幾則訊息。
所以使用:
LEFT JOIN
以及:
COUNT()
接下來分別說明。
今天使用:
LEFT JOIN conversation_messages m
ON c.id = m.conversation_id
這代表我們要把:
conversations
和:
conversation_messages
兩張資料表關聯起來。
例如:
conversations
-------------------
A
B
C
而訊息資料表是:
conversation_messages
-------------------
A → Message 1
A → Message 2
B → Message 3
其中:
Conversation C
目前沒有任何訊息。
如果使用 INNER JOIN,沒有訊息的對話可能就不會出現在結果中。
但今天使用:
LEFT JOIN
所以即使對話沒有訊息,也可以保留在查詢結果裡。
例如:
| conversationId | messageCount |
|---|---|
| A | 2 |
| B | 1 |
| C | 0 |
這樣比較符合對話列表的需求。
接著看到:
COUNT(m.id) AS message_count
這裡使用:
COUNT()
計算訊息數量。
為什麼使用:
COUNT(m.id)
而不是:
COUNT(*)
呢?
因為我們使用了 LEFT JOIN。
如果某段對話沒有訊息,LEFT JOIN 仍然會保留一筆對話資料。
使用 COUNT(*) 可能會把這筆資料算進去。
但:
COUNT(m.id)
只會計算實際存在的訊息 ID。
所以沒有訊息的對話會得到:
0
接著:
GROUP BY c.id, c.created_at
會按照每段對話進行分組。
最後:
ORDER BY c.created_at DESC, c.id DESC
讓較新的對話排在前面。
這樣我們就完成對話列表的 SQL 查詢。
接下來要完成第二個功能:
GET /health/ai/conversations/{id}
例如:
GET /health/ai/conversations/550e8400-e29b-41d4-a716-446655440000
我們希望回傳:
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"messages": [
{
"role": "user",
"content": "什麼是 BMI?"
},
{
"role": "assistant",
"content": "BMI 是身體質量指數。"
},
{
"role": "user",
"content": "那它怎麼計算?"
},
{
"role": "assistant",
"content": "體重除以身高的平方。"
}
]
}
這樣就可以取得一段對話的完整內容。
未來如果要製作網頁聊天介面,也可以直接使用這個 API 顯示歷史訊息。
在:
dto
Package 裡面新增:
ConversationDetailResponse.java
完整程式如下:
package com.example.healthassistant.dto;
import java.util.List;
public class ConversationDetailResponse {
private String conversationId;
private List<ConversationMessage> messages;
public ConversationDetailResponse(
String conversationId,
List<ConversationMessage> messages) {
this.conversationId = conversationId;
this.messages = messages;
}
public String getConversationId() {
return conversationId;
}
public List<ConversationMessage> getMessages() {
return messages;
}
}
這裡我們直接沿用昨天建立的:
ConversationMessage
DTO。
所以不需要再另外建立一個訊息類別。
目前的結構會變成:
ConversationDetailResponse
│
├── conversationId
│
└── messages
├── ConversationMessage
├── ConversationMessage
├── ConversationMessage
└── ConversationMessage
這樣的設計也方便未來擴充。
昨天的 ConversationMessage 只有:
role
content
今天既然要建立完整聊天紀錄查詢功能,我想再加入:
id
createdAt
這樣未來如果要在網頁上顯示訊息時間,或是區分不同訊息,就會比較方便。
打開:
ConversationMessage.java
將內容更新成:
package com.example.healthassistant.dto;
import java.time.LocalDateTime;
public class ConversationMessage {
private Long id;
private String role;
private String content;
private LocalDateTime createdAt;
public ConversationMessage(
Long id,
String role,
String content,
LocalDateTime createdAt) {
this.id = id;
this.role = role;
this.content = content;
this.createdAt = createdAt;
}
public Long getId() {
return id;
}
public String getRole() {
return role;
}
public String getContent() {
return content;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
}
這次新增了:
private Long id;
以及:
private LocalDateTime createdAt;
因為原本的建構子已經改變,所以 Repository 裡面建立 ConversationMessage 的程式,也需要一起更新。
打開:
ConversationRepository.java
找到昨天建立的:
findMessages()
將它改成:
public List<ConversationMessage> findMessages(
String conversationId) {
String sql = """
SELECT id, role, content, created_at
FROM conversation_messages
WHERE conversation_id = ?
ORDER BY id
""";
return jdbcTemplate.query(
sql,
(rs, rowNum) -> new ConversationMessage(
rs.getLong("id"),
rs.getString("role"),
rs.getString("content"),
rs.getTimestamp("created_at")
.toLocalDateTime()
),
conversationId
);
}
這次多查詢了:
id
以及:
created_at
這樣就能取得完整訊息資訊。
另外,昨天建立的:
findRecentMessages()
也要一起修改。
更新成:
public List<ConversationMessage> findRecentMessages(
String conversationId,
int limit) {
String sql = """
SELECT id, role, content, created_at
FROM (
SELECT id, role, content, created_at
FROM conversation_messages
WHERE conversation_id = ?
ORDER BY id DESC
LIMIT ?
) recent
ORDER BY id
""";
return jdbcTemplate.query(
sql,
(rs, rowNum) -> new ConversationMessage(
rs.getLong("id"),
rs.getString("role"),
rs.getString("content"),
rs.getTimestamp("created_at")
.toLocalDateTime()
),
conversationId,
limit
);
}
這樣 Day 26 的多輪對話功能也可以繼續使用。
因為 AIConversationService 原本使用的是:
message.getRole()
以及:
message.getContent()
這兩個 Getter 仍然保留,所以不需要修改 AI 對話邏輯。
接下來我們需要一個 Service,負責管理對話資料。
在:
service
Package 裡面新增:
ConversationManagementService.java
這個 Service 會負責:
查詢所有對話
查詢指定對話
以及:
刪除指定對話
完整程式如下:
package com.example.healthassistant.service;
import com.example.healthassistant.dto.ConversationDetailResponse;
import com.example.healthassistant.dto.ConversationListResponse;
import com.example.healthassistant.dto.ConversationMessage;
import com.example.healthassistant.exception.ConversationNotFoundException;
import com.example.healthassistant.repository.ConversationRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class ConversationManagementService {
private final ConversationRepository conversationRepository;
public ConversationManagementService(
ConversationRepository conversationRepository) {
this.conversationRepository = conversationRepository;
}
public List<ConversationListResponse> getAllConversations() {
return conversationRepository.findAllConversations();
}
public ConversationDetailResponse getConversation(
String conversationId) {
if (!conversationRepository.exists(conversationId)) {
throw new ConversationNotFoundException(
"Conversation not found."
);
}
List<ConversationMessage> messages =
conversationRepository.findMessages(conversationId);
return new ConversationDetailResponse(
conversationId,
messages
);
}
@Transactional
public void deleteConversation(String conversationId) {
if (!conversationRepository.exists(conversationId)) {
throw new ConversationNotFoundException(
"Conversation not found."
);
}
conversationRepository.deleteMessages(conversationId);
conversationRepository.deleteConversation(conversationId);
}
}
這個 Service 的設計很簡單。
Controller 不需要直接操作資料庫。
而是透過:
ConversationManagementService
呼叫:
ConversationRepository
這樣可以讓不同層級的責任更加清楚。
不過目前 Repository 還沒有刪除功能。
所以接下來就要完成 DELETE 的 SQL。
還記得昨天我們建立的外鍵嗎?
FOREIGN KEY (conversation_id)
REFERENCES conversations(id)
這個外鍵會確保:
每一則訊息都屬於一段存在的對話。
但也代表:
不能隨便刪除仍然有訊息的對話。
例如:
Conversation A
│
├── Message 1
├── Message 2
├── Message 3
└── Message 4
如果我們直接執行:
DELETE FROM conversations
WHERE id = 'A';
資料庫可能會拒絕這個操作。
因為:
conversation_messages
仍然有資料參照這段對話。
所以今天採用:
先刪除訊息,再刪除對話。
流程如下:
DELETE conversation_messages
↓
DELETE conversations
這樣就不會違反外鍵限制。
打開:
ConversationRepository.java
加入第一個 Method:
public void deleteMessages(String conversationId) {
String sql = """
DELETE FROM conversation_messages
WHERE conversation_id = ?
""";
jdbcTemplate.update(
sql,
conversationId
);
}
這個 Method 會刪除指定對話的所有訊息。
接著加入第二個 Method:
public void deleteConversation(String conversationId) {
String sql = """
DELETE FROM conversations
WHERE id = ?
""";
jdbcTemplate.update(
sql,
conversationId
);
}
這個 Method 則會刪除對話本身。
最後我們就可以在 Service 裡面依序呼叫:
conversationRepository.deleteMessages(conversationId);
conversationRepository.deleteConversation(conversationId);
這樣就完成基本的刪除流程。
今天在:
deleteConversation()
加入:
@Transactional
原因是我們需要執行兩個 SQL 操作。
第一個:
DELETE FROM conversation_messages
第二個:
DELETE FROM conversations
我們希望這兩個操作要嘛全部成功,要嘛在發生錯誤時一起回復。
例如:
刪除訊息成功
↓
刪除對話失敗
如果沒有交易管理,就可能留下不一致的結果。
但使用:
@Transactional
後,在一般未捕捉的執行期例外情況下,Spring 可以將同一筆交易中的資料庫變更回滾。
這樣就能降低部分資料不一致的風險。
而且今天的:
ConversationManagementService
是由 Controller 呼叫的獨立 Service。
因此交易可以透過 Spring 的代理機制生效。
這和昨天提到的「同一個物件內部直接呼叫交易 Method」不同。
今天的查詢與刪除功能,都可能遇到:
Conversation not found.
也就是找不到指定對話。
之前我們使用:
IllegalArgumentException
但這個例外的用途比較廣泛。
例如參數格式錯誤,也可能使用它。
所以今天決定建立一個更明確的例外類別。
在:
exception
Package 裡面新增:
ConversationNotFoundException.java
完整程式如下:
package com.example.healthassistant.exception;
public class ConversationNotFoundException
extends RuntimeException {
public ConversationNotFoundException(String message) {
super(message);
}
}
這樣我們就可以明確表示:
指定對話不存在。
接著在:
GlobalExceptionHandler.java
加入:
@ExceptionHandler(ConversationNotFoundException.class)
public ResponseEntity<Map<String, String>>
handleConversationNotFound(
ConversationNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(
Map.of("error", ex.getMessage())
);
}
記得確認有以下 Import:
import com.example.healthassistant.exception.ConversationNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.Map;
如果原本已經有:
GlobalExceptionHandler
只要新增 Method 即可,不需要重新建立整個類別。
這樣找不到對話時,就會回傳:
404 Not Found
例如:
{
"error": "Conversation not found."
}
另外,Day 26 的 AIConversationService 也可以將原本的:
throw new IllegalArgumentException(
"Conversation not found."
);
改成:
throw new ConversationNotFoundException(
"Conversation not found."
);
並加入對應 Import。
這樣整個專案就能使用一致的例外處理方式。
接下來終於要建立今天的三個 API!
打開:
AIController.java
今天不需要刪除原本的 AI 功能。
我們只要新增:
ConversationManagementService
即可。
首先新增 Import:
import com.example.healthassistant.dto.ConversationListResponse;
import com.example.healthassistant.dto.ConversationDetailResponse;
import com.example.healthassistant.service.ConversationManagementService;
import java.util.List;
接著在 Controller 裡面新增欄位:
private final ConversationManagementService conversationManagementService;
並將它加入原本的建構子參數:
ConversationManagementService conversationManagementService
在建構子內指定:
this.conversationManagementService =
conversationManagementService;
最後加入以下三個 API Method。
@GetMapping("/conversations")
public ResponseEntity<List<ConversationListResponse>>
getAllConversations() {
List<ConversationListResponse> conversations =
conversationManagementService.getAllConversations();
return ResponseEntity.ok(conversations);
}
@GetMapping("/conversations/{id}")
public ResponseEntity<ConversationDetailResponse>
getConversation(@PathVariable String id) {
ConversationDetailResponse conversation =
conversationManagementService.getConversation(id);
return ResponseEntity.ok(conversation);
}
@DeleteMapping("/conversations/{id}")
public ResponseEntity<Void>
deleteConversation(@PathVariable String id) {
conversationManagementService.deleteConversation(id);
return ResponseEntity.noContent().build();
}
這樣就完成今天的 Controller!
今天的 API 使用:
@PathVariable
例如:
@GetMapping("/conversations/{id}")
其中:
{id}
代表 URL 中的一個變數。
假設使用者呼叫:
GET /health/ai/conversations/123
Spring Boot 就會把:
123
帶入:
@PathVariable String id
所以:
id = "123";
這樣我們就可以使用 URL 指定要查詢的對話。
之前我們比較常使用:
@RequestParam
例如:
GET /health/water/date?date=2026-10-17
兩者的差異如下:
| 註解 | 用途 | 範例 |
|---|---|---|
@RequestParam |
取得查詢參數 | ?date=2026-10-17 |
@PathVariable |
取得 URL 路徑變數 | /conversations/123 |
今天因為要操作指定的對話資源,所以使用 @PathVariable。
今天第一次在專案中使用:
@DeleteMapping
例如:
@DeleteMapping("/conversations/{id}")
它對應 HTTP 的:
DELETE
主要用來刪除指定資源。
另外,今天成功刪除後會回傳:
ResponseEntity.noContent().build();
代表:
204 No Content
意思是:
請求已成功處理,但沒有需要回傳的內容。
例如:
DELETE /health/ai/conversations/123
如果成功:
204 No Content
Response Body 為空。
這也是 REST API 常見的刪除回應方式。
接下來重新啟動 Spring Boot。
先確認資料庫中已經有一些對話。
如果還沒有,可以先使用之前的:
POST /health/ai/conversation
建立幾段測試對話。
接著打開 Postman。
HTTP Method:
GET
URL:
http://localhost:8080/health/ai/conversations
按下 Send。
如果查詢成功,預期回傳:
[
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2026-10-17T14:00:00",
"messageCount": 4
},
{
"conversationId": "9c1e4b20-2f7a-4c88-a2d3-123456789abc",
"createdAt": "2026-10-17T14:05:00",
"messageCount": 2
}
]
實際 ID、時間及訊息數量會依照資料庫內容而不同。
如果目前沒有任何對話,則會回傳:
[]
這是正常的。
因為:
沒有資料
不代表:
API 發生錯誤
接著測試第二個 API。
HTTP Method:
GET
URL:
http://localhost:8080/health/ai/conversations/550e8400-e29b-41d4-a716-446655440000
請將 URL 最後面的 UUID 替換成實際存在的 conversationId。
如果查詢成功,預期回傳類似:
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"messages": [
{
"id": 1,
"role": "user",
"content": "什麼是 BMI?",
"createdAt": "2026-10-17T14:00:01"
},
{
"id": 2,
"role": "assistant",
"content": "BMI 是身體質量指數。",
"createdAt": "2026-10-17T14:00:03"
},
{
"id": 3,
"role": "user",
"content": "那它怎麼計算?",
"createdAt": "2026-10-17T14:01:00"
},
{
"id": 4,
"role": "assistant",
"content": "BMI 的計算方式是體重除以身高的平方。",
"createdAt": "2026-10-17T14:01:02"
}
]
}
這樣就可以取得完整的聊天內容。
未來如果要建立前端介面,就可以將這些訊息依照 role 分別顯示。
例如:
User → 靠右顯示
Assistant → 靠左顯示
就會比較接近一般聊天應用程式。
接下來故意輸入一個不存在的 ID。
例如:
GET /health/ai/conversations/not-exist-id
因為資料庫中找不到這段對話,所以 Service 會拋出:
ConversationNotFoundException
接著由:
GlobalExceptionHandler
統一處理。
預期 HTTP Status:
404 Not Found
Response Body:
{
"error": "Conversation not found."
}
這樣使用者就可以知道:
指定的對話不存在。
而不是只看到不明原因的伺服器錯誤。
接下來是今天最重要的刪除功能!
先使用:
GET /health/ai/conversations
確認資料庫中有哪幾段對話。
選擇一段測試用對話。
接著在 Postman 設定:
HTTP Method:
DELETE
URL:
http://localhost:8080/health/ai/conversations/550e8400-e29b-41d4-a716-446655440000
請使用實際存在的測試對話 ID。
按下 Send。
如果刪除成功,預期回傳:
204 No Content
而且不會有 Response Body。
接著再呼叫:
GET /health/ai/conversations
就會發現剛剛刪除的對話已經不在列表中。
如果再次查詢被刪除的對話:
GET /health/ai/conversations/550e8400-e29b-41d4-a716-446655440000
預期會得到:
404 Not Found
這樣就完成刪除功能的測試!
雖然 API 已經回傳:
204 No Content
但我們還可以進一步使用 H2 Console 確認資料。
打開:
http://localhost:8080/h2-console
登入後執行:
SELECT *
FROM conversations;
確認指定對話已經不存在。
接著執行:
SELECT *
FROM conversation_messages;
確認屬於該對話的訊息也已經刪除。
也可以使用更精確的查詢:
SELECT *
FROM conversation_messages
WHERE conversation_id = '你的實際對話ID';
如果刪除成功,應該查不到任何資料。
這樣就能確認:
我們不只刪除了對話本身,也刪除了對應的聊天訊息。
今天刪除對話時,我們使用:
先刪除訊息
再刪除對話
但其實資料庫還有另一種做法:
ON DELETE CASCADE
例如:
FOREIGN KEY (conversation_id)
REFERENCES conversations(id)
ON DELETE CASCADE
這代表:
當某段對話被刪除時,資料庫會自動刪除屬於該對話的訊息。
例如:
DELETE Conversation A
↓
自動刪除 Message 1
↓
自動刪除 Message 2
↓
自動刪除 Message 3
這樣就不需要在 Java 裡面分別執行兩個 DELETE。
不過今天沒有直接修改原本的 Foreign Key。
原因是:
我們的 H2 Database 已經建立了資料表。
即使修改:
schema.sql
裡面的 CREATE TABLE IF NOT EXISTS,也不會自動更新既有外鍵設定。
因此今天先使用明確的兩步驟刪除方式。
這樣也比較容易理解外鍵和交易的關係。
雖然今天已經完成對話管理功能,但目前仍然是本機教學版本。
目前:
GET /health/ai/conversations
會查詢資料庫中的所有對話。
如果未來有多個使用者,這樣就可能造成隱私問題。
正式系統應該只允許使用者查詢自己的對話。
例如:
User A → 只能查看 Conversation A
User B → 只能查看 Conversation B
所以未來需要加入登入驗證與資料存取權限。
即使 UUID 不容易被猜到,也不能把它當成登入憑證。
知道某個對話 ID,不應該直接代表有權限查看或刪除該對話。
目前:
GET /health/ai/conversations
會一次回傳所有對話。
如果未來有幾千筆資料,回傳內容就可能非常龐大。
所以正式系統可以加入:
page
以及:
size
例如:
GET /health/ai/conversations?page=0&size=10
每次只取得部分資料。
目前只要呼叫 DELETE API,就會直接刪除資料。
如果未來有前端介面,應該先讓使用者確認。
例如:
確定要刪除這段對話嗎?
避免誤刪。
目前是單機教學架構。
如果未來部署多個 Spring Boot 實例,仍然需要考慮同時新增訊息、刪除對話,以及資料庫交易之間的並行問題。
今天新增了:
ConversationListResponse.java
ConversationDetailResponse.java
ConversationManagementService.java
ConversationNotFoundException.java
並修改:
ConversationMessage.java
ConversationRepository.java
AIController.java
GlobalExceptionHandler.java
目前專案結構如下:
health-assistant
│
├── src/main/java/com/example/healthassistant
│ │
│ ├── HealthAssistantApplication.java
│ │
│ ├── controller
│ │ ├── HealthController.java
│ │ ├── WaterController.java
│ │ ├── SleepController.java
│ │ ├── HealthSummaryController.java
│ │ └── AIController.java
│ │
│ ├── service
│ │ ├── BmiService.java
│ │ ├── WaterService.java
│ │ ├── SleepService.java
│ │ ├── HealthSummaryService.java
│ │ ├── AIService.java
│ │ ├── AIHealthSummaryService.java
│ │ ├── AIChatService.java
│ │ ├── AIConversationService.java
│ │ ├── ConversationPersistenceService.java
│ │ └── ConversationManagementService.java
│ │
│ ├── repository
│ │ ├── WaterRepository.java
│ │ ├── SleepRepository.java
│ │ └── ConversationRepository.java
│ │
│ ├── dto
│ │ ├── HealthRequest.java
│ │ ├── WaterRequest.java
│ │ ├── WaterHistoryResponse.java
│ │ ├── WaterStatisticsResponse.java
│ │ ├── SleepRequest.java
│ │ ├── SleepResponse.java
│ │ ├── SleepHistoryResponse.java
│ │ ├── SleepStatisticsResponse.java
│ │ ├── WaterSummaryResponse.java
│ │ ├── SleepSummaryResponse.java
│ │ ├── HealthSummaryResponse.java
│ │ ├── AIHealthSummaryResponse.java
│ │ ├── AIChatRequest.java
│ │ ├── AIChatResponse.java
│ │ ├── ConversationRequest.java
│ │ ├── ConversationResponse.java
│ │ ├── ConversationMessage.java
│ │ ├── ConversationListResponse.java
│ │ └── ConversationDetailResponse.java
│ │
│ └── exception
│ ├── GlobalExceptionHandler.java
│ └── ConversationNotFoundException.java
│
├── src/main/resources
│ ├── application.properties
│ └── schema.sql
│
└── pom.xml
目前智慧健康助理的 AI 對話功能已經包含:
建立對話
↓
多輪聊天
↓
保存資料庫
↓
查詢對話列表
↓
查看完整聊天紀錄
↓
刪除對話
和 Day 24 剛建立單次問答時相比,功能真的完整了不少!
今天成功讓智慧健康助理加入 AI 對話管理功能!
原本我們只能透過 conversationId 接續聊天。
現在已經可以查詢所有對話、查看完整歷史訊息,以及刪除不需要的對話。
今天完成的功能包括:
ConversationListResponse DTOConversationDetailResponse DTOConversationMessage DTOLEFT JOIN 查詢對話列表COUNT() 計算訊息數量GROUP BY 彙整對話資訊ConversationManagementService
ConversationNotFoundException
@PathVariable 取得對話 ID@Transactional 保護刪除流程現在我們的 AI 功能已經變成:
智慧健康助理
│
├── BMI 計算與分類
│
├── 飲水量紀錄
│ ├── 新增紀錄
│ ├── 歷史查詢
│ └── 七天統計
│
├── 睡眠時間紀錄
│ ├── 新增紀錄
│ ├── 指定日期查詢
│ └── 七天統計
│
├── 每日健康摘要
│
└── AI 功能
├── AI API 串接測試
├── AI 每日健康摘要
├── AI 單次健康問答
├── AI 多輪對話
├── AI 對話資料庫保存
└── AI 對話管理
├── 查詢所有對話
├── 查詢完整紀錄
└── 刪除指定對話
今天最大的收穫是:
建立聊天機器人不只是讓 AI 可以回答問題,還需要讓使用者能夠查詢、管理與刪除自己的對話資料。
而且透過今天的練習,我們又進一步熟悉了 Spring Boot、Spring JDBC、REST API,以及資料庫關聯設計。
距離 30 天挑戰結束,真的只剩最後三天了!
到目前為止,我們所有的功能幾乎都是透過 Postman 測試。
雖然 API 已經越來越完整,但如果想讓一般使用者操作,總不能要求每個人都先學會 Postman 吧 XD
所以下一篇準備挑戰:
建立智慧健康助理的簡易網頁介面!
預計會學習:
fetch() 呼叫 APIconversationId 以支援連續聊天讓我們的智慧健康助理終於不只是一組 API,而是有一個可以實際操作的聊天介面!